package com.ryan.ohos.extension.compact;

import ohos.agp.components.Component;
import ohos.agp.components.Component.TouchEventListener;
import ohos.agp.components.VelocityDetector;
import ohos.multimodalinput.event.TouchEvent;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * 用于修复鸿蒙{@link TouchEvent}内的一些数据错误，并矫正由此产生的一些计算错误，如：使用{@link VelocityDetector}计算速度等。
 * 此类适用于单手指移动的情况，暂未适配多手指，也不适用于{@link TouchEvent#getPointerScreenPosition(int)}。
 * 在{@link TouchEventListener#onTouchEvent(Component, TouchEvent)}内调用{@link #correct(TouchEvent)}即可。
 * 注意：请勿重复或在滑动过程中构造此对象，构造函数中使用了反射，可能会造成性能问题。
 */
public class TouchEventCompact {

    private Field originActionCancel;

    private Method offsetLocationMethod;
    private Method setActionMethod;
    private Method getPointerIdBitsMethod;
    private Field ohosField;

    private float downY, downScreenY;
    private boolean corrected = false;
    private float offsetY = 0;
    private boolean alignToScreenPosition;
    private final float[] origin;

    /**
     * 不要反复构造对象
     */
    public TouchEventCompact() {
        this(false);
    }

    /**
     * 不要反复构造对象
     *
     * @param alignToScreenPosition 参考{@link #setAlignToScreenPosition(boolean)}
     */
    public TouchEventCompact(boolean alignToScreenPosition) {
        origin = new float[2];
        this.alignToScreenPosition = alignToScreenPosition;
        try {
            Class<?> origin = Class.forName("android.view.MotionEvent");
            for (Method declaredMethod : origin.getDeclaredMethods()) {
                if (declaredMethod.getName().equals("offsetLocation")) {
                    declaredMethod.setAccessible(true);
                    offsetLocationMethod = declaredMethod;
                }

                if (declaredMethod.getName().equals("setAction")) {
                    declaredMethod.setAccessible(true);
                    setActionMethod = declaredMethod;
                }

                if (declaredMethod.getName().equals("getPointerIdBits")) {
                    declaredMethod.setAccessible(true);
                    getPointerIdBitsMethod = declaredMethod;
                }
            }

            originActionCancel = origin.getDeclaredField("ACTION_CANCEL");

            Class<?> impl = Class.forName("ohos.multimodalinput.eventimpl.TouchEventImpl");
            ohosField = impl.getDeclaredField("motionEvent");
            ohosField.setAccessible(true);

        } catch (ClassNotFoundException | NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    /**
     * 是否将{@link TouchEvent#getPointerPosition(int)}获取的坐标与{@link TouchEvent#getPointerScreenPosition(int)}设置为一致。
     * 在使用{@link VelocityDetector}计算速度时，可以避免控件自身位置的改变，造成{@link TouchEvent#getPointerPosition(int)}
     * 获取的坐标异常，而导致速度计算有误
     *
     * @param alignToScreenPosition 是否一致
     */
    public void setAlignToScreenPosition(boolean alignToScreenPosition) {
        this.alignToScreenPosition = alignToScreenPosition;
    }

    /**
     * 修正{@link TouchEvent}的x、y轴偏移
     * @param ev 待修正的TouchEvent
     */
    public void correct(TouchEvent ev) {
        float x = getX(ev);
        float y = getY(ev);
        origin[0] = x;
        origin[1] = y;

        float screenX = getScreenX(ev);
        float screenY = getScreenY(ev);

        switch (ev.getAction()) {
            case TouchEvent.PRIMARY_POINT_DOWN:
                recycle();

                downY = y;
                downScreenY = screenY;
                break;

            case TouchEvent.POINT_MOVE:
                if (!corrected) {
                    float actualOffsetY = downScreenY - screenY;
                    offsetY = downY - y - actualOffsetY;
                    corrected = true;
                }
                motionEventOffsetLocation(ev, 0, offsetY);
                break;

            case TouchEvent.PRIMARY_POINT_UP:
            case TouchEvent.CANCEL:
                recycle();
                break;
        }

        if (alignToScreenPosition) {
            motionEventOffsetLocation(ev, screenX - x, screenY - y);
        }
    }

    /**
     * 获取原始通过{@link TouchEvent#getPointerPosition(int)}获取的坐标
     * @return 原始坐标
     */
    public float[] getOrigin() {
        return origin;
    }

    /**
     * 调整MotionEvent的位置
     * @param ev 包含MotionEvent的TouchEvent
     * @param deltaX Amount to add to the current X coordinate of the event.
     * @param deltaY Amount to add to the current Y coordinate of the event.
     */
    public void motionEventOffsetLocation(TouchEvent ev, float deltaX, float deltaY) {
        try {
            Object mv = ohosField.get(ev);
            offsetLocationMethod.invoke(mv, deltaX, deltaY);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 设置MotionEvent的action
     * @param ev 包含MotionEvent的TouchEvent
     * @param action 需要设置的action
     */
    public void motionEventSetAction(TouchEvent ev, int action) {
        try {
            Object mv = ohosField.get(ev);
            setActionMethod.invoke(mv, action);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取MotionEvent内的ACTION_CANCEL的值
     * @param ev 包含MotionEvent的TouchEvent
     * @return ACTION_CANCEL的值
     */
    public int getOriginActionCancel(TouchEvent ev) {
        try {
            Object mv = ohosField.get(ev);
            return originActionCancel.getInt(mv);
        } catch (Exception e) {
            e.printStackTrace();
            return 3;
        }
    }

    public int getOriginPointerIdBits(TouchEvent ev) {
        try {
            Object mv = ohosField.get(ev);
            return (int) getPointerIdBitsMethod.invoke(mv);
        } catch (IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }

        return 0;
    }

    /**
     * 重置相关数据
     */
    public void recycle() {
        downY = 0;
        corrected = false;
        offsetY = 0;
    }

    private float getY(TouchEvent ev) {
        return ev.getPointerPosition(ev.getIndex()).getY();
    }

    private float getX(TouchEvent ev) {
        return ev.getPointerPosition(ev.getIndex()).getX();
    }

    private float getScreenY(TouchEvent ev) {
        return ev.getPointerScreenPosition(ev.getIndex()).getY();
    }

    private float getScreenX(TouchEvent ev) {
        return ev.getPointerScreenPosition(ev.getIndex()).getX();
    }
}
